package io.noties.markwon.base


import android.widget.TextView
import android.text.Spanned
import android.content.Context
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.widget.TextView.BufferType
import io.noties.markwon.base.core.CorePlugin
import io.noties.markwon.theme.MarkwonCategoryTheme
import org.commonmark.node.Node
import org.commonmark.parser.Parser
import java.lang.IllegalStateException
import java.util.Locale

/**
 * Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted
 * of static stateless methods). An instance of builder can be obtained via [.builder]
 * method.
 *
 * @see .create
 * @see .builder
 * @see io.noties.markwon.Markwon.Builder
 */
interface Markwon {
    /**
     * Method to parse markdown (without rendering)
     *
     * @param input markdown input to parse
     * @return parsed via commonmark-java `org.commonmark.node.Node`
     * @see .render
     * @since 3.0.0
     */
     fun parse(input: String): Node

    /**
     * Create Spanned markdown from parsed Node (via [.parse] call).
     *
     *
     * Please note that returned Spanned has few limitations. For example, images, tables
     * and ordered lists require TextView to be properly displayed. This is why images and tables
     * most likely won\'t work in this case. Ordered lists might have mis-measurements. Whenever
     * possible use [.setMarkdown] or [.setParsedMarkdown]
     * as these methods will additionally call specific [io.noties.markwon.MarkwonPlugin] methods to *prepare*
     * proper display.
     *
     * @since 3.0.0
     */
     fun render(node: Node): Spanned

    /**
     * This method will [.parse] and [.render] supplied markdown. Returned
     * Spanned has the same limitations as from [.render] method.
     *
     * @param input markdown input
     * @see .parse
     * @see .render
     * @since 3.0.0
     */
     fun toMarkdown(input: String): Spanned
     fun setMarkdown(textView: TextView, markdown: String)
     fun setParsedMarkdown(textView: TextView, markdown: Spanned)

    /**
     * Requests information if certain plugin has been registered. Please note that this
     * method will check for super classes also, so if supplied with `markwon.hasPlugin(MarkwonPlugin.class)`
     * this method (if has at least one plugin) will return true. If for example a custom
     * (subclassed) version of a [CorePlugin] has been registered and given name
     * `CorePlugin2`, then both `markwon.hasPlugin(CorePlugin2.class)` and
     * `markwon.hasPlugin(CorePlugin.class)` will return true.
     *
     * @param plugin type to query
     * @return true if a plugin is used when configuring this [Markwon] instance
     */
     fun hasPlugin(plugin: Class<out MarkwonPlugin>): Boolean
     fun <P : MarkwonPlugin?> getPlugin(type: Class<P>): P?

    /**
     * @since 4.1.0
     */
     fun <P : MarkwonPlugin?> requirePlugin(type: Class<P>): P

    /**
     * @return a list of registered [MarkwonPlugin]
     * @since 4.1.0
     */
     var plugins: List<MarkwonPlugin>
     fun configuration(): MarkwonConfiguration

    /**
     * Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText
     * functionality
     *
     * @see PrecomputedTextSetterCompat
     *
     * @since 4.1.0
     */
    interface TextSetter {
        /**
         * @param textView   TextView
         * @param markdown   prepared markdown
         * @param bufferType BufferType specified when building [io.noties.markwon.Markwon] instance
         * via [io.noties.markwon.Markwon.Builder.bufferType]
         * @param onComplete action to run when set-text is finished (required to call in order
         * to execute [io.noties.markwon.MarkwonPlugin.afterSetText])
         */
        fun setText(
            textView: TextView,
            markdown: Spanned,
            bufferType: BufferType,
            onComplete: Runnable
        )
    }

    companion object {
        /**
         * Factory method to create a *minimally* functional [io.noties.markwon.Markwon] instance. This
         * instance will have **only** [CorePlugin] registered. If you wish
         * to configure this instance more consider using [.builder] method.
         *
         * @return [io.noties.markwon.Markwon] instance with only CorePlugin registered
         * @since 3.0.0
         */
        fun create(context: Context, theme: MarkwonCategoryTheme): Markwon {
            return builder(context, theme)
                //.usePlugin(CorePlugin.create())
                .build()
        }

        /**
         * Factory method to obtain an instance of [Builder] with [CorePlugin] added.
         *
         * @see Builder
         *
         * @see .builderNoCore
         * @since 3.0.0
         */
        fun builder(context: Context, theme: MarkwonCategoryTheme): MarkwonBuilder {
            return MarkwonBuilderImpl(context, theme) // @since 4.0.0 add CorePlugin
                //.usePlugin(CorePlugin.create())
        }

        /**
         * Factory method to obtain an instance of [io.noties.markwon.Markwon.Builder] without [CorePlugin].
         *
         * @since 4.0.0
         */
        fun builderNoCore(context: Context, theme: MarkwonCategoryTheme): MarkwonBuilder {
            return MarkwonBuilderImpl(context, theme)
        }
    }
}

/**
 * @since 3.0.0
 */
internal class MarkwonImpl(
    private val bufferType: BufferType,
    // @since 4.1.0
    private val textSetter: Markwon.TextSetter?,
    private val parser: Parser,
    // @since 4.1.1
    private val visitorFactory: MarkwonVisitorFactory,
    private val configuration: MarkwonConfiguration,
    override var plugins: List<MarkwonPlugin>,
    // @since 4.4.0
    private val fallbackToRawInputWhenEmpty: Boolean
) : Markwon {
    override fun parse(input: String): Node {

        // make sure that all plugins are called `processMarkdown` before parsing
        var inputStr = input
        for (plugin in plugins) {
            inputStr = plugin.processMarkdown(inputStr)
        }
        return parser.parse(inputStr)
    }

    override fun render(node: Node): Spanned {
        configuration.categoryTheme().initRender()

        plugins.forEach {
            it.beforeRender(node)
        }

        // @since 4.1.1 obtain visitor via factory
        val visitor = visitorFactory.create()
        node.accept(visitor)

        plugins.forEach {
            it.afterRender(node, visitor)
        }

        // clear render props and builder after rendering
        // @since 4.1.1 as we no longer reuse visitor - there is no need to clean it
        //  we might still do it if we introduce a thread-local storage though
//        visitor.clear();
        return visitor.builder().spannableStringBuilder()
    }

    override fun toMarkdown(input: String): Spanned {
        val node = parse(input)

        val spanned = render(node)

        // @since 4.4.0
        // if spanned is empty, we are configured to use raw input and input is not empty
        return if (TextUtils.isEmpty(spanned) && fallbackToRawInputWhenEmpty && !TextUtils.isEmpty(input)) {
            // let's use SpannableStringBuilder in order to keep backward-compatibility
            SpannableStringBuilder(input)
        } else spanned
    }

    override fun setMarkdown(textView: TextView, markdown: String) {
        setParsedMarkdown(textView, toMarkdown(markdown))
    }

    override fun setParsedMarkdown(textView: TextView, markdown: Spanned) {
        plugins.forEach {
            it.beforeSetText(textView, markdown)
        }
        // @since 4.1.0
        if (textSetter != null) {
            textSetter.setText(
                textView,
                markdown,
                bufferType,
                Runnable { // on-complete we just must call `afterSetText` on all plugins
                    plugins.forEach {
                        it.afterSetText(textView)
                    }
                })
        } else {
            // if no text-setter is specified -> just a regular sync operation
            textView.setText(markdown, bufferType)
            plugins.forEach {
                it.afterSetText(textView)
            }
        }
    }

    override fun hasPlugin(plugin: Class<out MarkwonPlugin>): Boolean {
        return getPlugin(plugin) != null
    }

    override fun <P : MarkwonPlugin?> getPlugin(type: Class<P>): P? {
        var out: MarkwonPlugin? = null
        for (plugin in plugins) {
            if (type.isAssignableFrom(plugin.javaClass)) {
                out = plugin
            }
        }
        return out as P?
    }

    override fun <P : MarkwonPlugin?> requirePlugin(type: Class<P>): P {
        return getPlugin(type)
            ?: throw IllegalStateException(
                String.format(
                    Locale.US, "Requested plugin `%s` is not " +
                            "registered with this Markwon instance", type.name
                )
            )
    }

    /* override fun getPlugins(): List<MarkwonPlugin> {
         return Collections.unmodifiableList(plugins)
     }*/

    override fun configuration(): MarkwonConfiguration {
        return configuration
    }
}